package fr.gla.hibtracker.historique.listener;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.hibernate.SessionFactory;
import org.hibernate.event.FlushEntityEvent;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.type.ComponentType;
import org.hibernate.type.EntityType;
import org.hibernate.type.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import fr.gla.hibtracker.historique.beans.ELProperty;

public class EventListenerHelper {

    private static final Logger LOGGER = LoggerFactory.getLogger(EventListenerHelper.class);

    /**
     * Verifie si 2 objets de meme type on été modifiés
     *
     * @param previousValue
     * @param newValue
     * @return
     */
    public static boolean isChanged(Object previousValue, Object newValue) {
        if ((previousValue == null && newValue != null) || (newValue == null && previousValue != null)) {
            return true;
        } else
            if (previousValue == null && newValue == null) {
                return false;
            } else {
                return !previousValue.equals(newValue);
            }

    }

    /**
     * Récupère les noms des propriétés d'un componentType(ie embeddedComponent)
     */
    public static List<String> getEmbeddedComponentPropertyName(ComponentType componentType) {
        return Arrays.asList(componentType.getPropertyNames());
    }

    /**
     * Récupère les noms des propriétés d'un entityComponent(ie OneToMany,ManyToOne ..)
     */
    public static List<String> getEntityComponentPropertyName(ClassMetadata classMetadata) {
        List<String> list = new ArrayList<String>(Arrays.asList(classMetadata.getPropertyNames()));
        list.add(classMetadata.getIdentifierPropertyName());
        return list;
    }

    /**
     * Verifie si la propriété contenue dans un embedded component est de type componentType(ie embeddedComponent)
     *
     * @param componentType
     * @param propertyName
     * @return
     */
    public static boolean isSubEmbeddedComponent(ComponentType componentType, String propertyName) {
        int indexOf = getEmbeddedComponentPropertyName(componentType).indexOf(propertyName);
        return indexOf != -1 && componentType.getSubtypes()[indexOf] instanceof ComponentType;
    }

    /**
     * Verifie si la propriété contenue dans un EntityComponent est de type EntityComponent(ie OneToMany,ManyToOne)
     *
     * @param classMetadata
     * @param propertyName
     * @return
     */
    public static boolean isSubEntityComponent(ClassMetadata classMetadata, String propertyName) {
        Type type = getSubType(classMetadata, propertyName);
        return type != null && type instanceof EntityType;
    }

    /**
     * Récupère le Type d'une propriété contenue dans un Entity component
     *
     * @param classMetadata
     * @param propertyName
     * @return
     */
    public static Type getSubType(ClassMetadata classMetadata, String propertyName) {
        return classMetadata.getPropertyType(propertyName);
    }

    public static EntityType getSubEntityComponent(ClassMetadata classMetadata, String propertyName) {
        return (EntityType) classMetadata.getPropertyType(propertyName);
    }

    /**
     * Récupère le componentType(ie embeddedComponent) d'une propriété contenue dans un embedded component
     *
     * @param componentType
     * @param propertyName
     * @return
     */
    public static ComponentType getSubEmbeddedComponent(ComponentType componentType, String propertyName) {
        return (ComponentType) getSubType(componentType, propertyName);
    }

    /**
     * Récupère le type d'une propriété contenue dans un embedded component
     *
     * @param componentType
     * @param propertyName
     * @return
     */
    public static Type getSubType(ComponentType componentType, String propertyName) {
        int indexOf = getEmbeddedComponentPropertyName(componentType).indexOf(propertyName);
        return (indexOf != -1) ? componentType.getSubtypes()[indexOf] : null;
    }

    /**
     * Verifie si la propertyName est de type ComponentType(ie embeddedComponent)
     *
     * @param persister
     * @param propertyName
     * @return
     */
    public static boolean isEmbeddedComponent(EntityPersister persister, String propertyName) {
        // getSessionFactoryImplementor().getReferencedPropertyType("de.bsweb.domain.Affaire", "offer")
        // persister.getPropertyType(propertyName).nullSafeSet(st, value, index, session)
        return persister.getPropertyType(propertyName) instanceof ComponentType;
    }

    /**
     * Verifie si la propertyName est de type entityType(ie OneToMany,ManyToOne .. )
     *
     * @param persister
     * @param propertyName
     * @return
     */
    public static boolean isEntityComponent(EntityPersister persister, String propertyName) {
        return persister.getPropertyType(propertyName) instanceof EntityType;
    }

    /**
     * Récupére un embedded component Depuis l'entityPersister
     *
     * @param persister
     * @param propertyName le nom de la propriété embedded
     * @return
     */
    public static EntityType getEntityComponent(EntityPersister persister, String propertyName) {
        return (EntityType) persister.getPropertyType(propertyName);
    }

    /**
     * Récupére un embedded component Depuis l'entityPersister
     *
     * @param persister
     * @param propertyName le nom de la propriété embedded
     * @return
     */
    public static ComponentType getEmbeddedComponent(EntityPersister persister, String propertyName) {
        return (ComponentType) persister.getPropertyType(propertyName);
    }

    public interface ElPropertyInitiatorHandler<T extends ELProperty> {
        void initiate(FlushEntityEvent event, T elProperty, String el, Object previousValue, Object newValue);
    }

    public static class EmptyElPropertyInitiatorHandler<T extends ELProperty> implements ElPropertyInitiatorHandler<T> {
        public void initiate(FlushEntityEvent event, T elProperty, String el, Object previousValue, Object newValue) {
        }
    }


    public static <T extends ELProperty> void checkDirtyProperties(FlushEntityEvent event,
                                                                   Object previousValue,
                                                                   Object newValue,
                                                                   Type type,
                                                                   Class<T> dirtyPropertyClass,
                                                                   List<T> dirtyElProperties,
                                                                   String el) {
        checkDirtyProperties(event,
                             previousValue,
                             newValue,
                             type,
                             dirtyPropertyClass,
                             new EmptyElPropertyInitiatorHandler<T>(),
                             dirtyElProperties,
                             el);
    }


    /**
     * Check recurssivement les propriétés modifiés d'un Component Permet de résoudre le fait qu'hibernate gère les
     * Component dont la référence a été modifié comme une seule et unique dirty entity Ex : si address.rue est modifié,
     * hibernate detecte uniquement que address est modifié et non la modification propriété rue de address. Ce qui
     * entraine que pour hibernate address.voie,address.cp.... ont été modifié(faille dans la granularité des
     * modifications de la référence sur un EntityComponent) Procédure : -Pour chacune des propriétés -On verifie si
     * elle est de type EntityComponent(ie OneToMany,ManyToOne ..) -si oui : on rapelle checkDirtyProperties pour
     * checker les modification de ce Component -si non : on ajoute a la liste des dirtyProperties la nouvelle
     * propriété(ssi elle à été modifié)
     *
     * @param dirtyElProperties la liste des dirtyProperties trouvés
     */
    public static <T extends ELProperty> void checkDirtyProperties(FlushEntityEvent event,
                                                                   Object previousValue,
                                                                   Object newValue,
                                                                   Type type,
                                                                   Class<T> dirtyPropertyClass,
                                                                   ElPropertyInitiatorHandler<T> elPropertyIniatorHandler,
                                                                   List<T> dirtyElProperties,
                                                                   String el) {
        final SessionFactory sessionFactory = event.getSession().getSessionFactory();
        if (type instanceof EntityType) {
            ClassMetadata classMetaData = sessionFactory.getClassMetadata(((EntityType) type).getReturnedClass());
            if (classMetaData != null) {
                List<String> entityPropertyNames = getEntityComponentPropertyName(classMetaData);
                for (String entityPropertyName : entityPropertyNames) {
                    StringBuilder builder = new StringBuilder().append(el).append(".").append(entityPropertyName);
                    if (LOGGER.isTraceEnabled()) {
                        LOGGER.trace("treat property " + builder.toString());
                    }
                    // TODO correct this(infinite)
                    if (countMatches(el, entityPropertyName) > 2) {
                        return;
                    }
                    Type propertyType = classMetaData.getPropertyType(entityPropertyName);
                    boolean isBoolean = isBoolean(propertyType);
                    Object previousValueTmp = (previousValue == null) ? null
                                    : invokeGetterWithoutCheckedException(previousValue,
                                                                                         entityPropertyName,
                                                                                         isBoolean);// ReflexiveUtils.getWithoutCheckedException(previousEntityComponentValue,
                    // entityPropertyName);
                    Object newValueTmp = (newValue == null) ? null
                                    : invokeGetterWithoutCheckedException(newValue,
                                                                                         entityPropertyName,
                                                                                         isBoolean);
                    if (isChanged(previousValueTmp, newValueTmp) /*
                                                                  * && isSubEntityComponent(classMetaData,
                                                                  * entityPropertyName)
                                                                  */) {
                        checkDirtyProperties(event,
                                             previousValueTmp,
                                             newValueTmp,
                                             classMetaData.getPropertyType(entityPropertyName),
                                             dirtyPropertyClass,
                                             elPropertyIniatorHandler,
                                             dirtyElProperties,
                                             builder.toString());
                    }
                }
            }
        } else
            if (type instanceof ComponentType) {
                ComponentType componentType = (ComponentType) type;
                List<String> embeddedPropertyNames = getEmbeddedComponentPropertyName(componentType);
                for (String embeddedPropertyName : embeddedPropertyNames) {
                    StringBuilder builder = new StringBuilder().append(el).append(".").append(embeddedPropertyName);
                    if (LOGGER.isTraceEnabled()) {
                        LOGGER.trace("treat property " + builder.toString());
                    }
                    Object previousValueTmp = (previousValue == null) ? null
                                    : getWithoutCheckedException(previousValue, embeddedPropertyName);
                    Object newValueTmp = (previousValue == null) ? null
                                    : getWithoutCheckedException(newValue, embeddedPropertyName);
                    if (isChanged(previousValue, newValue) /*
                                                            * && isSubEmbeddedComponent((ComponentType)type,
                                                            * embeddedPropertyName)
                                                            */) {
                        int indexOf = Arrays.asList(componentType.getPropertyNames()).indexOf(embeddedPropertyName);
                        Type subType = componentType.getSubtypes()[indexOf];
                        checkDirtyProperties(event,
                                             previousValueTmp,
                                             newValueTmp,
                                             subType,
                                             dirtyPropertyClass,
                                             elPropertyIniatorHandler,
                                             dirtyElProperties,
                                             builder.toString());
                    }
                }
            } else {
                addDirtyElProperty(event,
                                   dirtyPropertyClass,
                                   elPropertyIniatorHandler,
                                   dirtyElProperties,
                                   el,
                                   previousValue,
                                   newValue);
            }
    }

    private static boolean isBoolean(Type propertyType) {
        return boolean.class.isAssignableFrom(propertyType.getReturnedClass())
                        || Boolean.class.isAssignableFrom(propertyType.getReturnedClass());
    }

    /**
     * Ajoute une dirtyElProprerty si la valeur à été modifié depuis son chargement en base de données
     *
     * @param dirtyElProperties
     * @param el
     * @param previousValue
     * @param newValue
     */
    public static <T extends ELProperty> void addDirtyElProperty(FlushEntityEvent event,
                                                                 Class<T> dirtyPropertyClass,
                                                                 ElPropertyInitiatorHandler<T> elPropertyInitiatorHandler,
                                                                 List<T> dirtyElProperties,
                                                                 String el,
                                                                 Object previousValue,
                                                                 Object newValue) {
        if (isChanged(previousValue, newValue)) {
            Constructor<T> constructor;
            try {
                constructor = dirtyPropertyClass.getConstructor(new Class[] { String.class, Object.class, Object.class });
                T t = constructor.newInstance(new Object[] { el, previousValue, newValue });
                elPropertyInitiatorHandler.initiate(event, t, el, previousValue, newValue);
                dirtyElProperties.add(t);
            } catch (Exception e) {
                throw new ReflectionAccessException(e);
            }
        }
    }

    /**
     * Ajoute une dirtyElProprerty si la valeur à été modifié depuis son chargement en base de données
     *
     * @param dirtyElProperties
     * @param el
     * @param previousValue
     * @param newValue
     */
    public static <T extends ELProperty> void addDirtyElProperty(FlushEntityEvent event,
                                                                 Class<T> dirtyPropertyClass,
                                                                 List<T> dirtyElProperties,
                                                                 String el,
                                                                 Object previousValue,
                                                                 Object newValue) {
        addDirtyElProperty(event,
                           dirtyPropertyClass,
                           new EmptyElPropertyInitiatorHandler<T>(),
                           dirtyElProperties,
                           el,
                           previousValue,
                           newValue);
    }

    public static <T extends ELProperty> Map<String, T> transform(List<T> list) {
        Map<String, T> map = new HashMap<String, T>();
        for (T t : list) {
            map.put(t.getElName(), t);
        }
        return map;
    }
    
    private static int countMatches(String str, String sub){
        if((str == null || str.length() == 0) || (sub == null || sub.length() == 0))
            return 0;
        int count = 0;
        for(int idx = 0; (idx = str.indexOf(sub, idx)) != -1; idx += sub.length())
            count++;

        return count;
    }
    
    public static Object getWithoutCheckedException(Object object, String fieldName){
        Object returnValue = null;
        Field field;
        try{
            field = getDeclaredField(object.getClass(), fieldName);
        }
        catch(NoSuchFieldException e1){
            throw new ReflectionAccessException(e1);
        }
        if(field != null){
            field.setAccessible(true);
            try{
                returnValue = field.get(object);
            }
            catch(IllegalArgumentException e){
                throw new ReflectionAccessException(e);
            }
            catch(IllegalAccessException e){
                throw new ReflectionAccessException(e);
            }
        }
        return returnValue;
    }
    
    public static Field getDeclaredField(Class clazz, String name)throws NoSuchFieldException{
	    try{
	        Field field = clazz.getDeclaredField(name);
	        return field;
	    }
	    catch(SecurityException e){
	        throw e;
	    }
	    catch(NoSuchFieldException e){
	        if(clazz.getSuperclass() != null)
	            return getDeclaredField(clazz.getSuperclass(), name);
	        else
	            throw e;
	    }
    }
    
    public static Object invokeGetterWithoutCheckedException(Object object, String fieldName, boolean isBoolean){
        Object value = null;
        try{
            if(object != null)
                if(isBoolean)
                    try{
                        value = invoke(getGetterMethod(object.getClass(), fieldName, "is"), object, new Object[0]);
                    }
                    catch(NoSuchMethodException e){
                        value = invoke(getGetterMethod(object.getClass(), fieldName, "get"), object, new Object[0]);
                    }
                else
                    value = invoke(getGetterMethod(object.getClass(), fieldName, "get"), object, new Object[0]);
        }
        catch(NoSuchMethodException e){
            throw new ReflectionAccessException(e);
        }
        return value;
    }

    public static Object invoke(Method method, Object instance, Object args[]){
        try{
            return method.invoke(instance, args);
        }
        catch(Exception ex){
            throw new RuntimeException(ex);
        }
    }
    
    public static Method getGetterMethod(Class clazz, String fieldName, String prefixGetter)throws NoSuchMethodException{
	    String methodName = MessageFormat.format("{0}{1}{2}", new Object[] {
	        prefixGetter, fieldName.substring(0, 1).toUpperCase(), fieldName.substring(1, fieldName.length())
	    });
	    try{
	        return clazz.getDeclaredMethod(methodName, new Class[0]);
	    }
	    catch(NoSuchMethodException exception){
	        if(clazz.getSuperclass() != null)
	            return getGetterMethod(clazz.getSuperclass(), fieldName, prefixGetter);
	        else
	            throw exception;
	    }
    }
    
    public static class ReflectionAccessException extends RuntimeException{

    	private static final long serialVersionUID = 4204910499226962873L;
    	
        public ReflectionAccessException(String message, Throwable throwable){
            super(message, throwable);
        }
        
        public ReflectionAccessException(Throwable throwable){
            super(throwable);
        }
        
        public ReflectionAccessException(String message){
            super(message);
        }
        
    }

}
